Skip to content

5편. vue 전체 예시 이해 (main.ts)

앞선 1~4편을 한 줄로 요약: 렌더는 effect다. 상태를 읽으면 track, 바꾸면 trigger, 그리고 배치로 다시 렌더. 5편에서는 main.ts를 기준으로 이 흐름을 실제 실행 순서대로 끝까지 따라가 본다. (Counter/TodoList 예제를 전제로 설명한다.)

0. 부팅: createApp(App).mount('#app')

  1. createApp(App)이 루트 VNode를 만들고, mount('#app')에서 컨테이너 DOM을 잡는다.

  2. render(rootVNode, container)patch(null, rootVNode, container)컴포넌트 분기mountComponent 진입.

  3. mountComponent 내부에서 updateComponent라는 함수를 만든다. 이게 바로 렌더 effect의 본체다.

  4. instance.update = effect(updateComponent)

    • 즉시 1회 실행 → 최초 렌더 완료.
    • 실행 중에 ref/reactive를 읽으면 track으로 렌더-상태 연결이 생긴다.

요점: 최초 렌더도 effect 실행의 결과다. mount가 effect를 등록하고 곧장 한 번 돌린다.

1. Counter의 라이프사이클(읽기 → 등록)

  • setup()에서 const state = reactive({ count: 0 }), const double = computed(() => state.count * 2).

  • render(ctx)가 실행되면 ctx.count, ctx.double을 읽는다.

    • ctx.count 접근 → Proxy gettrack(target=state, key='count')
    • ctx.double 접근 → computed.value getter → 필요 시 내부 runner()로 계산, 그리고 track(holder,'value')
  • 결과적으로 렌더 effect는 두 dep에 구독한다: (state,'count')(computedHolder,'value').

2. 클릭 한 번에 무슨 일이 일어나나(쓰기 → 통지 → 재렌더)

버튼을 눌러 state.count++ 하는 순간을 프레임 단위로 분해해보자.

  1. set 트랩: Proxy set에서 이전 값과 새 값을 비교(Object.is). 값이 바뀌면 trigger(state,'count').

  2. triggertargetMap.get(state).get('count')구독 중인 effect 집합(dep) 을 얻는다.

  3. 각 effect에 대해 e.scheduler ? e.scheduler() : queueJob(e) 호출

    • 렌더 effect는 특별한 스케줄러가 없으므로 배치 큐(Set) 에 들어간다.
  4. 현재 tick의 동기 코드가 끝난 뒤, microtask에서 flushJobs()가 돌아 중복 제거된 effect들을 순서대로 실행한다.

  5. 렌더 effect(updateComponent) 재실행 → 새 VNode 트리를 만들고 이전 트리와 patch로 비교 → 필요한 DOM만 갱신.

배치의 핵심: 클릭 여러 번을 같은 tick에서 연속으로 해도 렌더는 보통 한 번. 화면이 덜 흔들린다.

3. computed와의 상호작용

  • state.count가 바뀌면 computed 내부 effect의 스케줄러가 동작해 dirty = true 로만 바꾼다.
  • 렌더가 다시 실행되며 double.value를 읽을 때에만 실제 계산을 새로 돌린다(캐시 재활성화).
  • 따라서 computed는 "계산 결과를 구독하는 쪽"(여기선 렌더)에게 trigger(holder,'value')로만 신호를 보낸다.

4. TodoList 흐름(입력/배열 조작)

  • inputref(''):

    • onInput에서 ctx.input = e.target.value → ref setter → triggerRefValue(ref) → 렌더 effect 스케줄.
  • todosreactive<Todo[]>([]):

    • push/splice 호출 시 Proxy set(또는 length/인덱스 key)에서 trigger 발생.
  • 렌더 갱신 시 patchChildren간단 순차 diff 로 리스트를 업데이트한다.

실무 팁: 큰 리스트에서 key 기반 diff(LIS) 최적화를 넣으면 재정렬 비용을 크게 줄일 수 있다. 현재 구현은 학습 목적의 순차 비교다.

5. 스케줄 타이밍(왜 microtask인가)

  • 렌더 effect는 기본적으로 queueJob을 타고 microtask에서 flush된다.
  • 같은 tick 안에서 수십 번 상태를 바꿔도 렌더는 대부분 한 번으로 합쳐짐.
  • 애니메이션/측정이 필요하면 watch에 flush:'pre' 같은 스케줄을 줄 수 있다(렌더 전 실행).

6. 한 장으로 보는 시퀀스 다이어그램

User Click
  └─ onClick → state.count++
       └─ Proxy.set → trigger(state,'count')
            └─ dep(effects).forEach(queueJob)
                 └─ microtask: flushJobs()
                      └─ effect(updateComponent)
                           ├─ cleanup(old deps)
                           ├─ render(ctx)  // track 새로 수집
                           ├─ patch(oldTree, newTree)
                           └─ DOM 업데이트

요약

  • main.ts부팅 스위치다. mount에서 렌더 effect를 등록해 첫 화면을 그리고, 이후엔 상태 변화가 알아서 길을 잇는다.
  • Counter/TodoList는 ref/reactive/computed/watch가 각자 맡은 역할을 수행하며, 모든 길은 렌더 effect로 모인다.
  • 이 구조를 이해하면, 실제 Vue에서도 문제가 터졌을 때 어디서 track이 끊겼는지, 어떤 trigger가 과하게 불탔는지를 감으로 잡아낼 수 있다.